package com.yanyeori.framework.security.config;

import com.yanyeori.framework.security.annotation.Anonymous;
import com.yanyeori.framework.security.configprop.SecurityConfigProps;
import com.yanyeori.framework.security.constant.SecurityConst;
import com.yanyeori.framework.security.context.TtlThreadLocalSecurityContextHolderStrategy;
import com.yanyeori.framework.security.filter.AuthSuccessAfterFilter;
import com.yanyeori.framework.security.filter.AuthenticationFilter;
import com.yanyeori.framework.security.service.PermissionsService;
import com.yanyeori.framework.security.service.RateLimiterService;
import com.yanyeori.framework.security.service.TokenLoginService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.RegExUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.HttpMethod;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler;
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import java.util.HashSet;
import java.util.Set;
import java.util.regex.Pattern;

/**
 * Spring Security Auto Config
 *
 * @author chenkuan 2020/11/20
 */
@Slf4j
@Configuration
@ComponentScan(SecurityConst.PACKAGE_NAME)
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableAsync
@Import({ConfigFactory.class})
public class SecurityAutoConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private RequestMappingHandlerMapping requestMappingHandlerMapping;
    @Autowired
    private SecurityConfigProps config;
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private PermissionsService permissionsService;
    @Autowired
    private RateLimiterService rateLimiterService;

    @Value("${swagger.enabled:false}")
    private boolean swaggerEnable;

    /**
     * Anonymous注解的匿名接口
     */
    private static final Set<String> ANONYMOUS_URL_SET = new HashSet<>();
    private final Pattern PATTERN = Pattern.compile("\\{(.*?)}");
    private final String ASTERISK = "*";

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }

    @Autowired
    public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
        authenticationManagerBuilder
                // 设置UserDetailsService
                .userDetailsService(userDetailsService)
                // 使用BCrypt进行密码的hash
                .passwordEncoder(passwordEncoder());
    }

    @Bean
    public TokenLoginService tokenLoginService() throws Exception {
        return new TokenLoginService(authenticationManager());
    }

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                // 由于使用的是JWT，我们这里不需要csrf
                .cors().and().csrf().disable()
                // 认证失败处理类
                .exceptionHandling().accessDeniedHandler(new SecurityAccessDeniedHandler()).and()
                // 权限不足处理类
                .exceptionHandling().authenticationEntryPoint(new SecurityAuthenticationEntryPoint()).and()
                // 基于token，所以不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .authorizeRequests()
                // 对于获取token的rest api要允许匿名访问
                .antMatchers(SecurityConst.HTTP_ANON_API).permitAll()
                .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                // 配置文件中的匿名接口
                .antMatchers(config.getAnonApi()).permitAll()
                // @Anonymous注解的controller方法匿名
                .antMatchers(getAnonymousUrls()).permitAll()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();
        // 禁用缓存
        // httpSecurity.headers().cacheControl();
        // 退出登录处理器
        httpSecurity.logout().logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler());
        // 添加JWT filter 登录认证流程过滤器
        httpSecurity.addFilterBefore(new AuthenticationFilter(tokenLoginService(), permissionsService), UsernamePasswordAuthenticationFilter.class);
        // 添加Authentication认证成功之后的filter
        httpSecurity.addFilterAfter(new AuthSuccessAfterFilter(rateLimiterService, permissionsService), FilterSecurityInterceptor.class);
        log.debug("HttpSecurity registered successfully");

        //配置security context策略
        configSecurityContextHolderStrategy();
    }

    private void configSecurityContextHolderStrategy() {
        SecurityContextHolder.setStrategyName(TtlThreadLocalSecurityContextHolderStrategy.class.getName());
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        //默认匿名静态资源
        web.ignoring().antMatchers(SecurityConst.WEB_ANON_API);
        //Swagger资源匿名访问
        if (swaggerEnable) {
            web.ignoring().antMatchers(SecurityConst.WEB_ANON_API_SWAGGER);
        }
    }

    /**
     * 获取标有注解 @Anonymous 的访问路径
     */
    private String[] getAnonymousUrls() {
        requestMappingHandlerMapping.getHandlerMethods().forEach((requestMappingInfo, handlerMethod) -> {
            //获取Anonymous注解的方法路径，替代path variable为 *
            Anonymous method = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), Anonymous.class);
            if (method == null) {
                Anonymous controller = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), Anonymous.class);
                if (controller == null) return;
            }
            PatternsRequestCondition patternsCondition = requestMappingInfo.getPatternsCondition();
            if (CollectionUtils.isEmpty(patternsCondition.getPatterns())) return;
            patternsCondition.getPatterns().forEach(url -> ANONYMOUS_URL_SET.add(RegExUtils.replaceAll(url, PATTERN, ASTERISK)));
        });
        return ANONYMOUS_URL_SET.toArray(new String[0]);
    }

    public static Set<String> getAnonymousUrlSet() {
        return ANONYMOUS_URL_SET;
    }
}
